ext/pdo_pgsql: Fixed PDO::CURSOR_SCROLL statements closing a cursor that does not exist - #23490
ext/pdo_pgsql: Fixed PDO::CURSOR_SCROLL statements closing a cursor that does not exist#23490KentarouTakeda wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
--- /dev/null
+++ b/ext/pdo_pgsql/tests/cursor_scroll_failed_redeclare.phpt
@@ -0,0 +1,37 @@
+--TEST--
+PDO PgSQL PDO::CURSOR_SCROLL sends no CLOSE after a failed re-declare
+--EXTENSIONS--
+pdo_pgsql
+--SKIPIF--
+<?php
+require __DIR__ . '/config.inc';
+require dirname(__DIR__, 2) . '/pdo/tests/pdo_test.inc';
+PDOTest::skip();
+?>
+--FILE--
+<?php
+
+require_once __DIR__ . "/config.inc";
+
+$db = Pdo::connect($config['ENV']['PDOTEST_DSN']);
+
+$stmt = $db->prepare('SELECT CAST(:v AS int)', [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL]);
+$stmt->execute([':v' => '1']);
+
+try {
+ $stmt->execute([':v' => 'not an int']);
+} catch (PDOException $e) {
+ echo $e::class, ': ', $e->getCode(), PHP_EOL;
+}
+
+$db->beginTransaction();
+unset($stmt);
+
+$db->exec('SELECT 2');
+
+echo 'Done', PHP_EOL;
+
+?>
+--EXPECT--
+PDOException: 22P02
+DoneThere was a problem hiding this comment.
Thanks, your test failed as expected. I've changed the approach. is_prepared and is_cursor_declared now track the two states separately.
b5a49d1 to
0366dee
Compare
0366dee to
fa968a0
Compare
|
I had the wrong target branch. This bug is not new, so it needs to go to |
|
Some more tests :) --TEST--
PDO PgSQL PDO::CURSOR_SCROLL keeps track of a held cursor when the CLOSE before a re-declare fails
--EXTENSIONS--
pdo_pgsql
--SKIPIF--
<?php
require __DIR__ . '/config.inc';
require dirname(__DIR__, 2) . '/pdo/tests/pdo_test.inc';
PDOTest::skip();
?>
--FILE--
<?php
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
$db = PDOTest::test_factory(__DIR__ . '/common.phpt');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $db->prepare('SELECT CAST(:v AS int)', [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL]);
$stmt->execute([':v' => '1']);
$db->beginTransaction();
try {
$db->exec('SELECT 1 / 0');
} catch (PDOException $e) {
echo $e::class, ': ', $e->getCode(), PHP_EOL;
}
try {
$stmt->execute([':v' => '2']);
} catch (PDOException $e) {
echo $e::class, ': ', $e->getCode(), PHP_EOL;
}
$db->rollBack();
unset($stmt);
var_dump($db->query("SELECT count(*) FROM pg_cursors WHERE name LIKE 'pdo\_crsr\_%'")->fetchColumn());
?>
--EXPECT--
PDOException: 22012
PDOException: 25P02
string(1) "0"--TEST--
PDO PgSQL PDO::CURSOR_SCROLL sends no CLOSE for a cursor a rollback already destroyed
--EXTENSIONS--
pdo_pgsql
--SKIPIF--
<?php
require __DIR__ . '/config.inc';
require dirname(__DIR__, 2) . '/pdo/tests/pdo_test.inc';
PDOTest::skip();
?>
--FILE--
<?php
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
$db = PDOTest::test_factory(__DIR__ . '/common.phpt');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->beginTransaction();
$stmt = $db->prepare('SELECT 1', [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL]);
$stmt->execute();
$db->rollBack();
$db->beginTransaction();
unset($stmt);
$db->exec('SELECT 2');
echo 'Done', PHP_EOL;
?>
--EXPECT--
Done |
fa968a0 to
b1b5b2a
Compare
|
I've changed the approach again. With libpq >= 17 the cursor is closed with The same defect was in By the way, applying this to the |
…hat does not exist
b1b5b2a to
a86b3d2
Compare
|
for DEALLOCATE there is an attempt. Probably needs some fixing now looking at it. |
|
Regarding your PR, I think you re not too far from completion. |
| char *q = NULL; | ||
|
|
||
| if (S->is_prepared) { | ||
| if (S->is_cursor_declared) { |
There was a problem hiding this comment.
what happens when the server destroys the cursor ?
There was a problem hiding this comment.
With libpq >= 17 PQclosePortal() reports success even when the cursor is already gone, so the flag clears here.
Otherwise CLOSE fails, the flag is left set, but DECLARE re-creates the cursor. The failed CLOSE still reaches the server's log.
cursor_scroll_reexecute_after_rollback.phpt covers this.
| efree(q); | ||
| #else | ||
| PQclear(PQclosePortal(H->server, S->cursor_name)); | ||
| S->is_cursor_declared = false; |
There was a problem hiding this comment.
I think the fact that is done unconditionally is a problem.
There was a problem hiding this comment.
Right, the two branches were inconsistent. Fixed it.
65ba626
| or later).])],, | ||
| [$PGSQL_LIBS]) | ||
|
|
||
| PHP_CHECK_LIBRARY([pq], [PQclosePortal], |
There was a problem hiding this comment.
you might need to update config.w32 too.
There was a problem hiding this comment.
Thanks, I had missed that. Added.
b2f0351
| res = PQexec(H->server, cmd); | ||
| } | ||
|
|
||
| if (PQresultStatus(res) == PGRES_COMMAND_OK) { |
There was a problem hiding this comment.
--- a/ext/pdo_pgsql/pgsql_statement.c
+++ b/ext/pdo_pgsql/pgsql_statement.c
@@ -85,6 +85,10 @@ static bool pdo_pgsql_try_cmd(const char *cmd, pdo_pgsql_db_handle *H)
if (PQresultStatus(res) == PGRES_COMMAND_OK) {
result = true;
+ } else if (res) {
+ const char *sqlstate = pdo_pgsql_sqlstate(res);
+
+ result = sqlstate && !strcmp(sqlstate, "34000");
}
if (q) efree(q);There was a problem hiding this comment.
True, that handles cursor state correctly, thanks!
I checked that PQexec() returns 34000 for the whole SAVEPOINT ...; CLOSE ...; RELEASE ... string, not 25P02.
34000 is passed in from the call site, since we can use the helper with 26000 for DEALLOCATE.
The destructor of a statement created with
[PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL]tries to close the cursor even when it does not exist. This fixes that.The attempted close causes an error on the database server, but the destructor discards its result, so the error cannot be observed by the user. Apart from polluting the server's log, this is mostly harmless, but when it happens inside a transaction, it causes a strange situation where subsequent statements fail for a reason that cannot be observed.
is_preparedwas overloaded to also mean "cursor declared" and was never reset, so the cursor state now has its own flag. A close that fails no longer aborts the caller's transaction.